2016/07/06

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

golang の channel は他の言語に見ない独特のパラダイムを開発者に提供します。

単純にスレッド間でメッセージングをするだけでもC言語で書けばそこそこの量になったり、慣れていない人であればどう実装すればいいか分からないなんて事もあったと思います。しかし golang の goroutine/channel は、やっている内容の割にとても容易にスレッド間通信やキューイング、処理の受け待ち等を実装できる様になっています。尚、channel をどの様に適用したら良いかについては以下を参照下さい。

Big Sky :: Golang の channel の使い所

golang の特徴と言えば goroutine と channel ですが、その使いどころに悩む人もおられる様です。 goroutine は非同期に実行される処理、channel はその grout...

http://mattn.kaoriya.net/software/lang/go/20131112132831.htm

今日はその goroutine/channel を使ったテクニックを3つほど紹介したいと思います。

ポーリング

channel は一般的に goroutine 側で chan への送信を行い、メイン処理側で chan の受信を行う事が多いと思います。

package main

import (
    "time"
)

func main() {
    q := make(chan struct{})

    go func() {
        // 重たい処理
        time.Sleep(3 * time.Second)
        q <- struct{}{}
    }()

    // q に何か入るまで待つ
    <-q
}

しかしメイン処理側でも待ち時間でやりたい事があったりもします。そういった場合にもう一つ goroutine/channel を作って...という方法もアリですが実は channel のポーリングは len(q) を使って簡単に実装できます。len(q) はその時点で channel に溜まったキューの数を返します。アトミックです。

package main

import (
    "fmt"
    "time"
)

func main() {
    q := make(chan struct{}, 2)

    go func() {
        // 重たい処理
        time.Sleep(3 * time.Second)
        q <- struct{}{}
    }()

    for {
        if len(q) > 0 {
            break
        }

        // q に溜まるまで他の事をしたい
        time.Sleep(1 * time.Second)
        fmt.Println("何か")
    }
}

こうする事で、q が送信されるまで別の処理を実できる様になります。GUI のメッセージループと channel を組み合わせる場合などには有効な手段です。注意点としては make で channel を作成する際に、バッファ数を 2 以上に設定しなければならない点です。そうしないと len(q) は常に 0 を返し続けます。

キューイング

例えば channel の受信をトリガにしてデータベースに接続して更新処理を実行する処理を実装するとします。複数のトリガが同時に発生した場合、channel には更新リクエストが溜まっていく訳ですが出来れば1回のデータベース接続で溜まった分全ての更新リクエストを処理したいといったケースもあります。あり得るケースで言えばデータベース接続が遅いなど。そこで登場するのが time.After との組み合わせです。

package main

import (
    "fmt"
    "time"
)

func main() {
    q := make(chan string5)

    go func() {
        time.Sleep(3 * time.Second)
        q <- "foo"
    }()
    go func() {
        time.Sleep(3 * time.Second)
        q <- "bar"
    }()

    // ほぼ同時に2つトリガが発生するのが分かっている
    var cmds []string
    cmds = append(cmds, <-q) // まずは一つ受信する
wait_some:
    for {
        select {
        case cmd := <-q:
            // 1秒以内なら一緒に処理しちゃうよ
            cmds = append(cmds, cmd)
        case <-time.After(1 * time.Second):
            // 1秒過ぎたらもう受け付けないよ
            break wait_some
        }
    }
    for _, cmd := range cmds {
        fmt.Println(cmd)
    }
}

この様に time.After が作る1秒間タイマー(実際は channel)とコマンド要求 channel を同時に待つ事で簡単に実装できます。これを他の言語で実装しようと考えるとちょっと頭がクラッとしてしまうかもしれませんね。

ワーカー

goroutine/channel を使えばワーカースレッドも簡単に作れます。例えば仕事量が5個あり、それを3つのワーカースレッドで仕事を取り合う物を実装したい場合は以下の様に実装します。

package main

import (
    "fmt"
    "sync"
    "time"
)

func fetchURL(wg *sync.WaitGroup, q chan string) {
    // 注意点: ↑これポインタな。
    defer wg.Done()
    for {
        url, ok := <-// closeされると ok が false になる
        if !ok {
            return
        }

        fmt.Println("ダウンロード: ", url)
        time.Sleep(3 * time.Second)
    }
}
func main() {
    var wg sync.WaitGroup

    q := make(chan string5)

    // ワーカーを3つ作る
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go fetchURL(&wg, q)
    }

    // 同時には3つまでしか処理できない
    q <- "http://www.example.com"
    q <- "http://www.example.net"
    q <- "http://www.example.net/foo"
    q <- "http://www.example.net/bar"
    q <- "http://www.example.net/baz"
    close(q) // これ大事

    wg.Wait() // すべてのgoroutineが終了するのをまつ
}
channel の受信処理 url, ok := <-q は channel が閉じられると2つ目の戻り値に false を返します。なので各ワーカースレッドはそれが true である間は channel からジョブを受け取って処理すれば良い事になります。

可能性はまだまだある

goroutine/channel の可能性はまだまだあると思っています。軽量なのでどんどん使っていけば良いですし、「こんな簡単な物に channel を使うのは大げさだ」と考える必要もないと思います。例えば peco では内部で大量に channel が使われていますがキビキビと動作します。CUI と golang の channel については昔に書かせて頂いた Gopher Academy の Advent Calendar 2013 でも紹介しています。

まだまだ channel を使った技はありそうなので、イイカンジのテクニックが出来たらぜひ情報発信して皆で共有して欲しいです。

Posted at by